/* -------------------------------------------------------------------
 System       : DOS 3.3
 Environment  : Aztec C65 DOS 3.3 Shell
 Program      : rat.c
                Copyright (C) Bill Buckels 2013.
 Description  : open and display RATF's
 ------------------------------------------------------------------ */

#include <stdio.h>
#include <rwts.h>

/* globals for convenience */
char hbuf[80], fbuf[260], fname[31], myname[80], mycfg[80], rbuf[260];
int fsecs, ctrack, csector;
char vbuf[256], cbuf[256], mytype=0,myvalue=0;
int reclen=0,recnt=0,rfirst=-1,rlast=32767,rmax=32767,rall=0,rbytes=0;
int sl=6, dr=1, vol=254;
int fldtbl[260];
int dumptxt = 0, dumpall = 0;

/* turn a string to upper and strip hi-bits -
   this allows DOS 3.3 text or 7 bit text for config files */
strupr(str)
char *str;
{
    int i;
    char az;

    for (i = 0; str[i] != 0; i++) {
        az = str[i] & 0x7f;
        if (az > 96 && az < 123) str[i] = (az - 32);
        else str[i] = az;
    }
}

/* current slot,drive, and volume routine - sets defaults */
sdv()
{
    char *ps = 0xaa6a;
    char *pd = 0xaa68;
    char *pv = 0xb3c1;

    sl = (int) ps[0];
    dr = (int) pd[0];
    vol = (int) pv[0];

}

/* set the filename for compare from the catalog sector
   this gives us a c language string */
setfname(ptr)
char *ptr;
{

  int i;

  /* Copy the file name, stripping the Apple's high bit */

  /* Note: I make no attempt to remove control characters
           if any in the file name. */

  for (i = 0; i < 30; i++)
  {
    fname[i] = ptr[i] & 0x7f;
  }
  fname[30] = 0;

  /* remove trailing blanks */
  for (i = 29; i >= 0; i--)
  {
	if (fname[i] == 0x20) fname[i] = 0;
	else break;
  }

}

/* helper function - return the value for the new file type
   or -1 if the type specified on the command line is bogus */
getmytype(ch)
char ch;
{
	if (ch == 'T') return 0;
	if (ch == 'b') return 0x40;
	if (ch == 'a') return 0x20;
	if (ch == 'R') return 0x10;
	if (ch == 'S') return 0x08;
	if (ch == 'B') return 0x04;
	if (ch == 'A') return 0x02;
	if (ch == 'I') return 0x01;

	return -1;
}

/* validate and set target file type */
setmytype(ch)
char ch;
{

   int c;

   c = getmytype(ch);

   if (c < 0) {
	   mytype = 0;
	   return -1;
   }
   mytype = ch;
   myvalue = (char) c;

   return 0;

}

/* filefind walks through the file entries in a catalog sector
   and searches for an active file match.

   if no file match returns the next track in the catalog list.
   if this is 0 then no more catalog sectors.

   if a file match, updates the file type in the catalog sector
   buffer and returns -1;

   the catalog sector buffer is then ready to be written
   back to the catalog.

   */

findfile()
{
	struct catsect *c;
	struct fentry *fe;

	int i,j;

	for (i = 0, j=FINDFIRST; i < CATENTRIES; i++, j+= FINDNEXT) {
       fe = (struct fentry *)&cbuf[j];
       /* deleted file */
       if (fe->track == 0xff) continue;
       /* never used */
       if (fe->track == 0) continue;
       setfname(fe->name);
       /* if the name matches, update the filetype
          with the filetype from the commandline
          and return a status of -1 */
       if (strcmp(fname,myname) == 0) {
           /* set the new filetype */
           fe->type = myvalue;
           /* return without updating the global current
              track and sector values. (ctrack and csector).
              these must remain positioned at the current sector
              for the update to work. otherwise the disk
              will be corrupted... so outa' here rfn!
              */
           fsecs = (int)fe->sectors;
           return -1;
	   }
	}


	c = (struct catsect *)&cbuf[0];

	/* update next catalog track and sector */
	ctrack = (int)c->track;
	csector = (int)c->sector;

    /* return next catalog track, if there are no more
       then this will be 0 */
	return (int)c->track;

}

chtype()
{

	struct vtoc *v;
    int status;

    /* get first catalog track and sector from vtoc */
	status = rwts(0x11, 0, &vbuf[0], RWREAD, sl, dr, vol);

    if (status != 0) {
       /* Track $11/Sector $0F (17/15 */
       ctrack = 17;
       csector = 15;
       /*
 	    printf("VTOC error. rwts returned %d (%x HEX).!\n",status,status);
		return -1;
       */
    }
    else {
    	v = (struct vtoc *)&vbuf[0];
    	ctrack = (int)v->ctrack;
    	csector = (int)v->csector;
	}

    /* read catalog entries and update file type if found */
    for (;;) {
       	status = rwts(ctrack, csector, &cbuf[0], RWREAD, sl, dr, vol);
       	/* if any problem at all, we are done */
		if (status != 0) {
		   printf("CATALOG error. rwts returned %d (%x HEX).!\n",status,status);
		   return -1;
		}

        /* filefind will update ctrack and csector to drive
           the search each time it is called */
       	status = findfile();
       	if (status == 0) break; /* all done but no match */

       	if (status < 0) {
			/* write the new filetype back to the catalog */
			/* if any problem at all we are done */
			status = rwts(ctrack, csector, &cbuf[0], RWWRITE, sl, dr, vol);
			if (status != 0) {
			   printf("CATALOG error. rwts returned %d (%x HEX)!\n",status,status);
			   return -1;
			}
		    return 0; /* success! so return silently... */
		}

	}
	printf("%s not in catalog!\n",myname);
    return -1;
}


/* displays active text fields in a RATF of any length at all */
/* hi-bits have been stripped by the time it gets here */
shorec(i,rnum)
int i,rnum;
{

	char *ptr, cr[1], ch;
	int i, len, fld = 0, cnt = 0;
    int comma = 0;

    /* delimited file dump */
    if (rbytes!=0) {
        if (i == -1) {
			rbytes--;
			if (rbytes!=0)clear(&rbuf[0],',',rbytes);
			rbuf[rbytes] = 13;
			rbytes++;
			write(1,rbuf,rbytes);

		}
		cr[0] = 13;
        fbuf[0] = ',';
		for (i = 0; i< reclen; i++) {
			if (fldtbl[i] == -1) continue;
			if (fldtbl[i] == 0)continue;
            cnt++;
            ch = rbuf[i] &0x7f;
            if (ch == 13) ch = 0;
			fbuf[1] = ch;
			fbuf[2] = 0;
			i++;
			if (fbuf[1] == 0) {
			   if (cnt != 1) write(1,fbuf,1);
			   continue;
			}
			len = 2;
			fld = i;

			/* we have no guarantee that fields are terminated */
			for (;fld < reclen;fld++) {
				if (fldtbl[fld] != 0) break;
				ch = rbuf[fld] &0x7f;
				if (ch==13) ch = 0;
				fbuf[len] = ch;
				if (ch == 0) break;
				len++;
				fbuf[len] = 0;
			}

			if (cnt == 1) write(1,(char *)&fbuf[1],len-1);
			else write(1,fbuf,len);
		}
		write(1,cr,1);
		return;
	}

    if (i == -1) {
		if (dumptxt == 0) printf("========Deleted Record %d\n",rnum);
		return;
	}

    if (dumptxt == 0)printf("========Record %d\n",rnum);
    if (i < 0) return;
    ptr = (char *) &rbuf[i];


	for (;;) {
		if (i > reclen) break;
		if (*ptr != 0) {
			if (dumptxt == 0) {
				printf("%-3d: ",i);
			    puts(ptr);
			}
			else {
				if (comma == 1) printf(",%s",ptr);
				else printf("%s",ptr);
				comma = 1;
			}
			len = strlen(ptr);
			i+= len;
			ptr = (char *) &rbuf[i];
			continue;
		}
		*ptr++;
		i++;
	}
	if (comma == 1)printf("%c",13);

}



shorecs(fh)
int fh;
{
	int c, i, cnt = 0, active = 0, part = 0, pos, rnum=0;
	char ch;

    if (rbytes != 0) {
	   /* if we have a field map we need to organize it now.
	   	  if the fields are beyond the record bounds, we cancel
	   	  field mapping, and don't bother listing anything...

	   	  we couldn't check this before because we let
	   	  them use both a command line and a config file
	   	  to specify settings in random order, so we were
	   	  not able to consistently verify record bounds
	   	  when we accepted the field map.

	   	  If we are out of bounds It serves no purpose to
	   	  wait any longer. Making them type the whole mess
	   	  again is bad enough... the DOS 3.3 shell doesn't
	   	  have a command line history.

	   	  They should probably use a config file anyway.

	   */

	   	pos = -1;
        for (i = 0;i<reclen;i++) {
			if (fldtbl[i] != -1) {
				cnt++;
				if (pos == -1) pos = i;
			}
			else {
				if (pos!= -1) fldtbl[i] = 0;
			}
		}
		if (cnt != rbytes) {
			puts("Byte offsets are out of bounds or contain duplicates!");
			return;
		}
		if (hbuf[0] != 0) {
			cnt = strlen(hbuf);
			write(1,hbuf,cnt);
		}
	   	cnt = 0;
	}

	for (;;) {

        if (rnum >= rlast) break;
        if (cnt >= rmax) break;

        clear(&rbuf[0],257,0);
		c = read(fh,rbuf,reclen);
		if (c < 1) break;

		rnum++;
		if (rnum < rfirst) continue;
		cnt++;

		if (c != reclen) part=c;

        pos = -1;
		for (i = 0; i < c; i++) {
		   /* strip hi-bits  - note if writing back to these
		      hi-bits must be set again (apple II text format) */
		   ch = rbuf[i] & 0x7f;
		   if (ch == 13) ch = 0;
		   rbuf[i] = ch;
		   if (ch != 0) {
			  if (pos == -1) pos = i;
		   }
		}

		if (pos < 0 && rall == 0) continue;
		if (pos > -1)active++;
		shorec(pos,(rnum+1));

		if (c < reclen) break;

	}

	/* a field list causes records to be formatted for export so no
	   additional info probably wanted */
	if (rbytes == 0 && dumptxt == 0) {
		puts("========");
		printf("%d records read (L = %d) %d active.\n",cnt, reclen, active);
		if (part != 0) {
			printf("partial record - %d bytes...\n",part);
		}
	}

}


/* sets positions for byte offsets */
setpos(ptr)
char *ptr;
{
	int pos, c, d;

	if (rbytes != 0)return;

	pos = atoi(ptr);
	if (pos > -1 && pos < 256) {
		fldtbl[pos] = 1;
		rbytes ++;
	}

	for (c = 0, d=1; ptr[c] != 0; c++,d++) {
	   if (ptr[c] == ',' && ptr[d] != 0) {
		   	pos = atoi((char *)&ptr[d]);
		   	if (pos > -1 && pos < 256) {
				fldtbl[pos] = 1;
				rbytes ++;
			}
		}
	}
}

readcfg()
{
	FILE *fp=NULL;
	int c, value;
	char ch, *ptr;

	fp = fopen(mycfg,"r");

	if (NULL == fp) return;

    /* all config parameters must have a single letter
       command (same as command line) followed by = (an equals sign)
       values are required except for the a command (display all records)

       The config file also has some additional features, like field headers,
       which would make the Shell's command line too long if supported
       inetractively.

     */
	while (fgets(fbuf,256,fp) !=0) {
		c = strlen(fbuf);
		if (c < 1) break;
		ch = fbuf[1] &0x7f;
		/* skip comments and empty lines */
		if(ch != '=') continue;
		strupr(fbuf);
		ch = fbuf[0];
		if (ch == '-') {
		   dumptxt = 1;
		   continue;
		}
		if (ch == 'H') {
			strcpy(hbuf,(char *)&fbuf[2]);
			for (c=0;;) {
			    ch = hbuf[c] &0x7f;
			    if (ch == 13 || ch == 10) ch = 0;
			    hbuf[c] = ch;
			    if (ch == 0) {
					hbuf[c] = 13;
					hbuf[c+1] = 0;
					break;
				}
			    c++;
			}
			continue;
		}
		if (ch == 'A') {
			rall = 1;
			continue;
		}
		value = atoi((char *)&fbuf[2]);
		if (ch == 'B') {
			 if (value > -1 && value < 257) setpos((char *)&fbuf[2]);
			 continue;
		}
		if (ch == 'L') {
			if (value == 911) {
			   dumpall = 1;
			   continue;
			}
			if (value > 0 && value < 257) reclen = value;
			continue;
		}
		if (ch == 'R') {
			if (value > -1 && rfirst == -1) {
				rfirst = value;
				continue;
			}
			if (value > -1 && rlast == 32767) {
				rlast = value;
				if (rlast < rfirst) {
				   rlast = rfirst;
				   rfirst = value;
				}
			}
			continue;
		}
		if (ch == 'N') {
			if (value > -1 && rmax == 32767) {
				rmax = value;
			}
			continue;
		}
		if (ch != 'S' && ch != 'D' && ch != 'V' || (value < 1 || value > 256))
			continue;

		/* not validating further */
		if (ch == 'S' && value < 8)sl = value;
		if (ch == 'D' && value < 3) dr = value;
		if (ch == 'V') vol = value;
	}
	fclose(fp);
}



char *usage[] = {
"Rat(C) Copyright Bill Buckels 2013. All Rats Reserved.",
"Random Analysis Tool to determine record length, field layout, view data,",
"dump data and obtain stats for DOS 3.3 Random Access Text Files (RATF's).",
"Usage:    rat filename -l(record length 1-256) (-options)",
"          rat filename configfilename (-options can be in config file)",
"Options: -n(number of records to list) -r(from record 0-32767) -r(to record)",
"         -b(byte offset field list) i.e. -b0,10,20",
"          note: byte offset can be used to select fields of interest only",
"         -a select all records (by default only active records are displayed)",
"-s -d -v slot drive volume i.e. -s6 -d1 -v254",
"command line record length and options are preceded by a dash as shown.",
"config file options are of the form l=(record length), etc. (name=value pairs)",
"additional config file option i.e.      h=FieldName,FieldName,FieldName",
"field header option when used with i.e. b=0,10,20",
"Output:   7 bit ascii to the screen, can be redirected: rat > tofile",
NULL};

pusage()
{
	int i;

	for (i=0;NULL != usage[i];i++)puts(usage[i]);

}

main(argc, argv)
int argc;
char **argv;
{
	int fh, c,value,status, i, j, secs;
    char *ptr, ch;

    /* set-up some stuff to use later */
    fsecs = -1;
    sdv();
    mycfg[0] = 0;
    myname[0] = 0;
    hbuf[0] = 0;
    for (c = 0; c < 257; c++)fldtbl[c] = -1;

    /* parse command line - switch character required for options */
	for (c = 1; c < argc; c++) {

		if (argv[c][0] == '-') {
			ptr = (char *)&argv[c][1];
		}
		else {
			/* get filename */
			ptr = (char *)&argv[c][0];
			strupr(ptr);
			if (myname[0] == 0) {
				strcpy(myname,ptr);
				continue;
			}
			if (mycfg[0] == 0) {
				strcpy(mycfg,ptr);
			}
			continue;
		}


		strupr(ptr);
		ch = ptr[0];
		if (ch == '-') {
			dumptxt = 1;
			continue;
		}
		if (ch == 'A') {
			rall = 1;
			continue;
		}
		value = atoi((char *)&ptr[1]);

		if (ch == 'B') {
			 if (value > -1 && value < 257) setpos((char *)&ptr[1]);
			 continue;
		}
		if (ch == 'L') {
			if (value == 911) {
			   dumpall = 1;
			   continue;
			}
			if (value > 0 && value < 257) reclen = value;
			continue;
		}
		if (ch == 'R') {
			if (value > -1 && rfirst == -1) {
				rfirst = value;
				continue;
			}
			if (value > -1 && rlast == 32767) {
				rlast = value;
				if (rlast < rfirst) {
				   rlast = rfirst;
				   rfirst = value;
				}
			}
			continue;
		}
		if (ch == 'N') {
			if (value > -1 && rmax == 32767) {
				rmax = value;
			}
			continue;
		}

		if ((ch != 'S' && ch != 'D' && ch != 'V') || (value < 1 || value > 256)) {
			pusage();
			printf("Invalid arg: %s\n",ptr);
			exit(1);
		}

		/* not validating further  in parser */
		if (ch == 'S')sl = value;
		if (ch == 'D') dr = value;
		if (ch == 'V') vol = value;
	}

    /* RATF always required so exit if none */
	if (myname[0] ==0) {
		pusage();
		puts("Invalid or missing file name!");
		exit(1);
	}

    /* read config file if any for more options */
	if(mycfg[0] != 0) readcfg();

    /* record length always required 1-256 bytes */
    if (reclen == 0 && dumpall == 0) {
		pusage();
		puts("Invalid or missing record length!");
		exit(1);
	}

    /* Aztec C65 opens all text files as sequential text files
       so change to type 'S' before opening...

       if they have entered a bugus RATF, it will not exist in
       the catalog, and chtype() will fail.

       chtype() also removes the lock bit if any.

       */
	if (setmytype('S') != 0 || chtype()!= 0) {
		puts("Can't change RATF to file type S! Unable to Read!");
		exit(1);
	}

    /* filename is unlikely to be bogus at this point and
       since lock if any has been removed (see comment above)
       the open() function should not fail. */

	fh = open(myname,0);
	if (fh < 0) {
		printf("Can't open %s\n",myname);
	}
	else {
		if (dumpall == 0) {
			shorecs(fh);
		}
		else {
			secs = 0;
			for (;;) {
		   		c = read(fh,vbuf,256);
		   		secs+= 1;
		   		if (c < 1) break;
		   		j = 0;
		   		for (i = 0; i < c; i++) {
				    ch = vbuf[i] & 0x7f;
				    if (ch!=0) {
					  cbuf[j] = ch;
					  j++;
					}
					if (j !=0) write(1,cbuf,j);
				}
		   		if (c < 256) break;
		   		if (secs >= fsecs) break;
		   }

		}
		close(fh);
	}
	if (setmytype('T') != 0 || chtype()!= 0) {
        puts("Can't change RATF to file type S! Try again!");
		exit(1);
	}

	exit(0);

}

